const std = @import("std");

const Allocator = std.mem.Allocator;
const PathList = std.ArrayList([]u8);

const max_file_bytes: usize = 16 * 1024 * 1024;

const TermColor = struct {
    pub const red = "\x1b[0;31m";
    pub const green = "\x1b[0;32m";
    pub const reset = "\x1b[0m";
};

pub fn main() !void {
    var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa_impl.deinit();
    const gpa = gpa_impl.allocator();

    var stdout_buffer: [4096]u8 = undefined;
    var stdout_state = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_state.interface;

    var found_errors = false;

    // Lint 1: Check for pub declarations without doc comments
    try stdout.print("Checking for pub declarations without doc comments...\n", .{});

    var zig_files = PathList{};
    defer freePathList(&zig_files, gpa);

    try walkTree(gpa, "src", &zig_files);

    for (zig_files.items) |file_path| {
        const errors = try checkPubDocComments(gpa, file_path);
        defer gpa.free(errors);

        if (errors.len > 0) {
            try stdout.print("{s}", .{errors});
            found_errors = true;
        }
    }

    if (found_errors) {
        try stdout.print("\n", .{});
        try stdout.print("Please add doc comments to the spots listed above, they make the code easier to understand for everyone.\n", .{});
        try stdout.print("\n", .{});
        try stdout.flush();
        std.process.exit(1);
    }

    // Lint 2: Check for top level comments in new Zig files
    try stdout.print("Checking for top level comments in new Zig files...\n", .{});

    var new_zig_files = try getNewZigFiles(gpa);
    defer {
        for (new_zig_files.items) |path| {
            gpa.free(path);
        }
        new_zig_files.deinit(gpa);
    }

    if (new_zig_files.items.len == 0) {
        try stdout.print("{s}[OK]{s} All lints passed!\n", .{ TermColor.green, TermColor.reset });
        try stdout.flush();
        return;
    }

    var failed_files = PathList{};
    defer freePathList(&failed_files, gpa);

    for (new_zig_files.items) |file_path| {
        if (!try fileHasTopLevelComment(gpa, file_path)) {
            try stdout.print("Error: {s} is missing top level comment (//!)\n", .{file_path});
            try failed_files.append(gpa, try gpa.dupe(u8, file_path));
        }
    }

    if (failed_files.items.len > 0) {
        try stdout.print("\n", .{});
        try stdout.print("The following files are missing a top level comment:\n", .{});
        for (failed_files.items) |path| {
            try stdout.print("    {s}\n", .{path});
        }
        try stdout.print("\n", .{});
        try stdout.print("Add a //! comment BEFORE any other code that explains the purpose of the file.\n", .{});
        try stdout.flush();
        std.process.exit(1);
    }

    try stdout.print("{s}[OK]{s} All lints passed!\n", .{ TermColor.green, TermColor.reset });
    try stdout.flush();
}

fn walkTree(allocator: Allocator, dir_path: []const u8, zig_files: *PathList) !void {
    var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
    defer dir.close();

    var it = dir.iterate();
    while (try it.next()) |entry| {
        if (entry.kind == .sym_link) continue;

        const next_path = try std.fs.path.join(allocator, &.{ dir_path, entry.name });

        switch (entry.kind) {
            .directory => {
                // Skip .zig-cache directories
                if (std.mem.eql(u8, entry.name, ".zig-cache")) {
                    allocator.free(next_path);
                    continue;
                }
                defer allocator.free(next_path);
                try walkTree(allocator, next_path, zig_files);
            },
            .file => {
                if (std.mem.endsWith(u8, entry.name, ".zig")) {
                    try zig_files.append(allocator, next_path);
                } else {
                    allocator.free(next_path);
                }
            },
            else => allocator.free(next_path),
        }
    }
}

fn checkPubDocComments(allocator: Allocator, file_path: []const u8) ![]u8 {
    const source = readSourceFile(allocator, file_path) catch |err| {
        // Skip files we can't read
        if (err == error.FileNotFound) return try allocator.dupe(u8, "");
        return err;
    };
    defer allocator.free(source);

    var errors = std.ArrayList(u8){};
    errdefer errors.deinit(allocator);

    var line_num: usize = 1;
    var prev_line: []const u8 = "";
    var lines = std.mem.splitScalar(u8, source, '\n');

    while (lines.next()) |line| {
        defer {
            prev_line = line;
            line_num += 1;
        }

        // Check if line starts with "pub " (no leading whitespace - only top-level declarations)
        if (!std.mem.startsWith(u8, line, "pub ")) continue;

        // Check if previous line is a doc comment (allow indented doc comments)
        const prev_trimmed = std.mem.trimLeft(u8, prev_line, " \t");
        if (std.mem.startsWith(u8, prev_trimmed, "///")) continue;

        // Skip exceptions: init, deinit, @import, and pub const re-exports
        // Note: "pub.*fn init\(" in bash matches "init" anywhere in function name
        if (std.mem.indexOf(u8, line, "fn init") != null) continue;
        if (std.mem.indexOf(u8, line, "fn deinit") != null) continue;
        if (std.mem.indexOf(u8, line, "@import") != null) continue;

        // Check for pub const re-exports (e.g., "pub const Foo = bar.Baz;")
        if (isReExport(line)) continue;

        try errors.writer(allocator).print("{s}:{d}: pub declaration without doc comment `///`\n", .{ file_path, line_num });
    }

    return errors.toOwnedSlice(allocator);
}

fn isReExport(line: []const u8) bool {
    // Match pattern: pub const X = lowercase.something;
    // This detects re-exports like "pub const Foo = bar.Baz;"
    if (!std.mem.startsWith(u8, line, "pub const ")) return false;

    // Find the '=' sign
    const eq_pos = std.mem.indexOf(u8, line, "=") orelse return false;
    const after_eq = std.mem.trimLeft(u8, line[eq_pos + 1 ..], " \t");

    // Check if it starts with a lowercase letter (module reference)
    if (after_eq.len == 0) return false;
    const first_char = after_eq[0];
    if (first_char < 'a' or first_char > 'z') return false;

    // Check if it contains a dot and ends with semicolon (but not a function call)
    if (std.mem.indexOf(u8, after_eq, ".") == null) return false;
    if (std.mem.indexOf(u8, after_eq, "(") != null) return false;
    if (!std.mem.endsWith(u8, std.mem.trimRight(u8, after_eq, " \t"), ";")) return false;

    return true;
}

fn getNewZigFiles(allocator: Allocator) !PathList {
    var result = PathList{};
    errdefer {
        for (result.items) |path| {
            allocator.free(path);
        }
        result.deinit(allocator);
    }

    // Run git diff to get new files
    var child = std.process.Child.init(&.{ "git", "diff", "--name-only", "--diff-filter=A", "origin/main", "HEAD", "--", "src/" }, allocator);
    child.stdout_behavior = .Pipe;
    child.stderr_behavior = .Ignore;

    _ = child.spawn() catch {
        // Git not available or not in a repo - return empty list
        return result;
    };

    const stdout = child.stdout orelse return result;
    const output = stdout.readToEndAlloc(allocator, max_file_bytes) catch return result;
    defer allocator.free(output);

    const term = child.wait() catch return result;
    if (term.Exited != 0) return result;

    // Parse output line by line
    var lines = std.mem.splitScalar(u8, output, '\n');
    while (lines.next()) |line| {
        if (line.len == 0) continue;
        if (!std.mem.endsWith(u8, line, ".zig")) continue;

        try result.append(allocator, try allocator.dupe(u8, line));
    }

    return result;
}

fn fileHasTopLevelComment(allocator: Allocator, file_path: []const u8) !bool {
    const source = try readSourceFile(allocator, file_path);
    defer allocator.free(source);

    return std.mem.indexOf(u8, source, "//!") != null;
}

fn readSourceFile(allocator: Allocator, path: []const u8) ![:0]u8 {
    return try std.fs.cwd().readFileAllocOptions(
        allocator,
        path,
        max_file_bytes,
        null,
        std.mem.Alignment.of(u8),
        0,
    );
}

fn freePathList(list: *PathList, allocator: Allocator) void {
    for (list.items) |path| {
        allocator.free(path);
    }
    list.deinit(allocator);
}
